<?php

namespace Luracast\Restler\Data;

use Luracast\Restler\CommentParser;
use Luracast\Restler\Format\HtmlFormat;
use Luracast\Restler\RestException;
use Luracast\Restler\Scope;
use Luracast\Restler\Util;

/**
 * Default Validator class used by Restler.
 * It can be replaced by any
 * iValidate implementing class by setting Defaults::$validatorClass
 *
 * @category Framework
 * @package Restler
 * @author R.Arul Kumaran <arul@luracast.com>
 * @copyright 2010 Luracast
 * @license http://www.opensource.org/licenses/lgpl-license.php LGPL
 * @link http://luracast.com/products/restler/
 * @version 3.0.0rc5
 */
class Validator implements iValidate {
	public static $holdException = false;
	public static $exceptions = array ();
	
	/**
	 * Validate alphabetic characters.
	 *
	 * Check that given value contains only alphabetic characters.
	 *
	 * @param
	 *        	$input
	 * @param ValidationInfo $info        	
	 *
	 * @return string
	 *
	 * @throws Invalid
	 */
	public static function alpha($input, ValidationInfo $info = null) {
		if (ctype_alpha ( $input )) {
			return $input;
		}
		if ($info && $info->fix) {
			// remove non alpha characters
			return preg_replace ( "/[^a-z]/i", "", $input );
		}
		throw new Invalid ( 'Expecting only alphabetic characters.' );
	}
	
	/**
	 * Validate alpha numeric characters.
	 *
	 * Check that given value contains only alpha numeric characters.
	 *
	 * @param
	 *        	$input
	 * @param ValidationInfo $info        	
	 *
	 * @return string
	 *
	 * @throws Invalid
	 */
	public static function alphanumeric($input, ValidationInfo $info = null) {
		if (ctype_alnum ( $input )) {
			return $input;
		}
		if ($info && $info->fix) {
			// remove non alpha numeric and space characters
			return preg_replace ( "/[^a-z0-9 ]/i", "", $input );
		}
		throw new Invalid ( 'Expecting only alpha numeric characters.' );
	}
	
	/**
	 * Validate printable characters.
	 *
	 * Check that given value contains only printable characters.
	 *
	 * @param
	 *        	$input
	 * @param ValidationInfo $info        	
	 *
	 * @return string
	 *
	 * @throws Invalid
	 */
	public static function printable($input, ValidationInfo $info = null) {
		if (ctype_print ( $input )) {
			return $input;
		}
		if ($info && $info->fix) {
			// remove non printable characters
			return preg_replace ( '/[\x00-\x1F\x80-\xFF]/', '', $input );
		}
		throw new Invalid ( 'Expecting only printable characters.' );
	}
	
	/**
	 * Validate hexadecimal digits.
	 *
	 * Check that given value contains only hexadecimal digits.
	 *
	 * @param
	 *        	$input
	 * @param ValidationInfo $info        	
	 *
	 * @return string
	 *
	 * @throws Invalid
	 */
	public static function hex($input, ValidationInfo $info = null) {
		if (ctype_xdigit ( $input )) {
			return $input;
		}
		throw new Invalid ( 'Expecting only hexadecimal digits.' );
	}
	
	/**
	 * Validate Telephone number
	 *
	 * Check if the given value is numeric with or without a `+` prefix
	 *
	 * @param
	 *        	$input
	 * @param ValidationInfo $info        	
	 *
	 * @return string
	 *
	 * @throws Invalid
	 */
	public static function tel($input, ValidationInfo $info = null) {
		if (is_numeric ( $input ) && '-' != substr ( $input, 0, 1 )) {
			return $input;
		}
		throw new Invalid ( 'Expecting phone number, a numeric value ' . 'with optional `+` prefix' );
	}
	
	/**
	 * Validate Email
	 *
	 * Check if the given string is a valid email
	 *
	 * @param String $input        	
	 * @param ValidationInfo $info        	
	 *
	 * @return string
	 * @throws Invalid
	 */
	public static function email($input, ValidationInfo $info = null) {
		$r = filter_var ( $input, FILTER_VALIDATE_EMAIL );
		if ($r) {
			return $r;
		} elseif ($info && $info->fix) {
			$r = filter_var ( $input, FILTER_SANITIZE_EMAIL );
			return static::email ( $r );
		}
		throw new Invalid ( 'Expecting email in `name@example.com` format' );
	}
	
	/**
	 * Validate IP Address
	 *
	 * Check if the given string is a valid ip address
	 *
	 * @param String $input        	
	 * @param ValidationInfo $info        	
	 *
	 * @return string
	 * @throws Invalid
	 */
	public static function ip($input, ValidationInfo $info = null) {
		$r = filter_var ( $input, FILTER_VALIDATE_IP );
		if ($r)
			return $r;
		
		throw new Invalid ( 'Expecting IP address in IPV6 or IPV4 format' );
	}
	
	/**
	 * Validate Url
	 *
	 * Check if the given string is a valid url
	 *
	 * @param String $input        	
	 * @param ValidationInfo $info        	
	 *
	 * @return string
	 * @throws Invalid
	 */
	public static function url($input, ValidationInfo $info = null) {
		$r = filter_var ( $input, FILTER_VALIDATE_URL );
		if ($r) {
			return $r;
		} elseif ($info && $info->fix) {
			$r = filter_var ( $input, FILTER_SANITIZE_URL );
			return static::url ( $r );
		}
		throw new Invalid ( 'Expecting url in `http://example.com` format' );
	}
	
	/**
	 * MySQL Date
	 *
	 * Check if the given string is a valid date in YYYY-MM-DD format
	 *
	 * @param String $input        	
	 * @param ValidationInfo $info        	
	 *
	 * @return string
	 * @throws Invalid
	 */
	public static function date($input, ValidationInfo $info = null) {
		if (preg_match ( '#^(?P<year>\d{2}|\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})$#', $input, $date ) && checkdate ( $date ['month'], $date ['day'], $date ['year'] )) {
			return $input;
		}
		throw new Invalid ( 'Expecting date in `YYYY-MM-DD` format, such as `' . date ( "Y-m-d" ) . '`' );
	}
	
	/**
	 * MySQL DateTime
	 *
	 * Check if the given string is a valid date and time in YYY-MM-DD HH:MM:SS format
	 *
	 * @param String $input        	
	 * @param ValidationInfo $info        	
	 *
	 * @return string
	 * @throws Invalid
	 */
	public static function datetime($input, ValidationInfo $info = null) {
		if (preg_match ( '/^(?P<year>19\d\d|20\d\d)\-(?P<month>0[1-9]|1[0-2])\-' . '(?P<day>0\d|[1-2]\d|3[0-1]) (?P<h>0\d|1\d|2[0-3]' . ')\:(?P<i>[0-5][0-9])\:(?P<s>[0-5][0-9])$/', $input, $date ) && checkdate ( $date ['month'], $date ['day'], $date ['year'] )) {
			return $input;
		}
		throw new Invalid ( 'Expecting date and time in `YYYY-MM-DD HH:MM:SS` format, such as `' . date ( "Y-m-d H:i:s" ) . '`' );
	}
	
	/**
	 * Alias for Time
	 *
	 * Check if the given string is a valid time in HH:MM:SS format
	 *
	 * @param String $input        	
	 * @param ValidationInfo $info        	
	 *
	 * @return string
	 * @throws Invalid
	 */
	public static function time24($input, ValidationInfo $info = null) {
		return static::time ( $input, $info );
	}
	
	/**
	 * Time
	 *
	 * Check if the given string is a valid time in HH:MM:SS format
	 *
	 * @param String $input        	
	 * @param ValidationInfo $info        	
	 *
	 * @return string
	 * @throws Invalid
	 */
	public static function time($input, ValidationInfo $info = null) {
		if (preg_match ( '/^([01]?[0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]$/', $input )) {
			return $input;
		}
		throw new Invalid ( 'Expecting time in `HH:MM:SS` format, such as `' . date ( "H:i:s" ) . '`' );
	}
	
	/**
	 * Time in 12 hour format
	 *
	 * Check if the given string is a valid time 12 hour format
	 *
	 * @param String $input        	
	 * @param ValidationInfo $info        	
	 *
	 * @return string
	 * @throws Invalid
	 */
	public static function time12($input, ValidationInfo $info = null) {
		if (preg_match ( '/^([1-9]|1[0-2]|0[1-9]){1}(:[0-5][0-9])?\s?([aApP][mM]{1})?$/', $input )) {
			return $input;
		}
		throw new Invalid ( 'Expecting time in 12 hour format, such as `08:00AM` and `10:05:11`' );
	}
	
	/**
	 * Unix Timestamp
	 *
	 * Check if the given value is a valid timestamp
	 *
	 * @param String $input        	
	 * @param ValidationInfo $info        	
	 *
	 * @return int
	 * @throws Invalid
	 */
	public static function timestamp($input, ValidationInfo $info = null) {
		if (( string ) ( int ) $input == $input && ($input <= PHP_INT_MAX) && ($input >= ~ PHP_INT_MAX)) {
			return ( int ) $input;
		}
		throw new Invalid ( 'Expecting unix timestamp, such as ' . time () );
	}
	
	/**
	 * Validate the given input
	 *
	 * Validates the input and attempts to fix it when fix is requested
	 *
	 * @param mixed $input        	
	 * @param ValidationInfo $info        	
	 * @param null $full        	
	 *
	 * @throws \Exception
	 * @return array|bool|float|int|mixed|null|number|string
	 */
	public static function validate($input, ValidationInfo $info, $full = null) {
		$html = Scope::get ( 'Restler' )->responseFormat instanceof HtmlFormat;
		$name = $html ? "<strong>$info->label</strong>" : "`$info->name`";
		try {
			if (is_null ( $input )) {
				if ($info->required) {
					throw new RestException ( 400, "$name is required." );
				}
				return null;
			}
			$error = isset ( $info->message ) ? $info->message : "Invalid value specified for $name";
			
			// if a validation method is specified
			if (! empty ( $info->method )) {
				$method = $info->method;
				$info->method = '';
				$r = self::validate ( $input, $info );
				return $info->apiClassInstance->{$method} ( $r );
			}
			
			// when type is an array check if it passes for any type
			if (is_array ( $info->type )) {
				// trace("types are ".print_r($info->type, true));
				$types = $info->type;
				foreach ( $types as $type ) {
					$info->type = $type;
					try {
						$r = self::validate ( $input, $info );
						if ($r !== false) {
							return $r;
						}
					} catch ( RestException $e ) {
						// just continue
					}
				}
				throw new RestException ( 400, $error );
			}
			
			// patterns are supported only for non numeric types
			if (isset ( $info->pattern ) && $info->type != 'int' && $info->type != 'float' && $info->type != 'number') {
				if (! preg_match ( $info->pattern, $input )) {
					throw new RestException ( 400, $error );
				}
			}
			
			if (isset ( $info->choice )) {
				if (is_array ( $input )) {
					foreach ( $input as $i ) {
						if (! in_array ( $i, $info->choice )) {
							$error .= ". Expected one of (" . implode ( ',', $info->choice ) . ").";
							throw new RestException ( 400, $error );
						}
					}
				} elseif (! in_array ( $input, $info->choice )) {
					$error .= ". Expected one of (" . implode ( ',', $info->choice ) . ").";
					throw new RestException ( 400, $error );
				}
			}
			
			if (method_exists ( $class = get_called_class (), $info->type ) && $info->type != 'validate') {
				try {
					return call_user_func ( "$class::$info->type", $input, $info );
				} catch ( Invalid $e ) {
					throw new RestException ( 400, $error . '. ' . $e->getMessage () );
				}
			}
			
			switch ($info->type) {
				case 'int' :
				case 'float' :
				case 'number' :
					if (! is_numeric ( $input )) {
						$error .= '. Expecting ' . ($info->type == 'int' ? 'integer' : 'numeric') . ' value';
						break;
					}
					if ($info->type == 'int' && ( int ) $input != $input) {
						if ($info->fix) {
							$r = ( int ) $input;
						} else {
							$error .= '. Expecting integer value';
							break;
						}
					} else {
						$r = $info->numericValue ( $input );
					}
					if (isset ( $info->min ) && $r < $info->min) {
						if ($info->fix) {
							$r = $info->min;
						} else {
							$error .= ". Minimum required value is $info->min.";
							break;
						}
					}
					if (isset ( $info->max ) && $r > $info->max) {
						if ($info->fix) {
							$r = $info->max;
						} else {
							$error .= ". Maximum allowed value is $info->max.";
							break;
						}
					}
					return $r;
				
				case 'string' :
					if (! is_string ( $input )) {
						$error .= '. Expecting alpha numeric value';
						break;
					}
					if ($info->required && $input === '') {
						$error = "$name is required.";
						break;
					}
					$r = strlen ( $input );
					if (isset ( $info->min ) && $r < $info->min) {
						if ($info->fix) {
							$input = str_pad ( $input, $info->min, $input );
						} else {
							$char = $info->min > 1 ? 'characters' : 'character';
							$error .= ". Minimum $info->min $char required.";
							break;
						}
					}
					if (isset ( $info->max ) && $r > $info->max) {
						if ($info->fix) {
							$input = substr ( $input, 0, $info->max );
						} else {
							$char = $info->max > 1 ? 'characters' : 'character';
							$error .= ". Maximum $info->max $char allowed.";
							break;
						}
					}
					return $input;
				
				case 'bool' :
				case 'boolean' :
					if ($input === 'true' || $input === true)
						return true;
					if (is_numeric ( $input ))
						return $input > 0;
					return false;
				
				case 'array' :
					if ($info->fix && is_string ( $input )) {
						$input = explode ( CommentParser::$arrayDelimiter, $input );
					}
					if (is_array ( $input )) {
						$contentType = Util::nestedValue ( $info, 'contentType' ) ?  : null;
						if ($info->fix) {
							if ($contentType == 'indexed') {
								$input = $info->filterArray ( $input, true );
							} elseif ($contentType == 'associative') {
								$input = $info->filterArray ( $input, true );
							}
						} elseif ($contentType == 'indexed' && array_values ( $input ) != $input) {
							$error .= '. Expecting a list of items but an item is given';
							break;
						} elseif ($contentType == 'associative' && array_values ( $input ) == $input && count ( $input )) {
							$error .= '. Expecting an item but a list is given';
							break;
						}
						$r = count ( $input );
						if (isset ( $info->min ) && $r < $info->min) {
							$item = $info->max > 1 ? 'items' : 'item';
							$error .= ". Minimum $info->min $item required.";
							break;
						}
						if (isset ( $info->max ) && $r > $info->max) {
							if ($info->fix) {
								$input = array_slice ( $input, 0, $info->max );
							} else {
								$item = $info->max > 1 ? 'items' : 'item';
								$error .= ". Maximum $info->max $item allowed.";
								break;
							}
						}
						if (isset ( $contentType ) && $contentType != 'associative' && $contentType != 'indexed') {
							$name = $info->name;
							$info->type = $contentType;
							unset ( $info->contentType );
							foreach ( $input as $key => $chinput ) {
								$info->name = "{$name}[$key]";
								$input [$key] = static::validate ( $chinput, $info );
							}
						}
						return $input;
					} elseif (isset ( $contentType )) {
						$error .= '. Expecting items of type ' . ($html ? "<strong>$contentType</strong>" : "`$contentType`");
						break;
					}
					break;
				case 'mixed' :
				case 'unknown_type' :
				case 'unknown' :
				case null : // treat as unknown
					return $input;
				default :
					if (! is_array ( $input )) {
						break;
					}
					// do type conversion
					if (class_exists ( $info->type )) {
						$input = $info->filterArray ( $input, false );
						$implements = class_implements ( $info->type );
						if (is_array ( $implements ) && in_array ( 'Luracast\\Restler\\Data\\iValueObject', $implements )) {
							return call_user_func ( "{$info->type}::__set_state", $input );
						}
						$class = $info->type;
						$instance = new $class ();
						if (is_array ( $info->children )) {
							if (empty ( $input ) || ! is_array ( $input ) || $input === array_values ( $input )) {
								$error .= '. Expecting an item of type ' . ($html ? "<strong>$info->type</strong>" : "`$info->type`");
								break;
							}
							foreach ( $info->children as $key => $value ) {
								$cv = new ValidationInfo ( $value );
								if (array_key_exists ( $key, $input ) || $cv->required) {
									$instance->{$key} = static::validate ( Util::nestedValue ( $input, $key ), $cv );
								}
							}
						}
						return $instance;
					}
			}
			throw new RestException ( 400, $error );
		} catch ( \Exception $e ) {
			static::$exceptions [] = $e;
			if (static::$holdException) {
				return null;
			}
			throw $e;
		}
	}
}